package com.fanyouzhao.eshop.portal.service.impl;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.fanyouzhao.eshop.core.pojo.Goods;
import com.fanyouzhao.eshop.core.service.impl.BaseServiceImpl;
import com.fanyouzhao.eshop.portal.exception.BusinessException;
import com.fanyouzhao.eshop.portal.mapper.GoodsMapper;
import com.fanyouzhao.eshop.portal.service.GoodsService;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @author: fanyouzhao
 * @date: 15:46 2021/7/2
 */
@Service("goodsService")
@Transactional(rollbackFor = Throwable.class)
public class GoodsServiceImpl extends BaseServiceImpl<Goods> implements GoodsService {

    @Resource
    RedisTemplate<String, Object> redisTemplate = null;
    @Resource
    private GoodsMapper goodsMapper;
    // 指定商品key的前缀
    private static final String GOODS_CACHE = "goods:";
    // 指定过期时间常量
    private static final Integer GOODS_CACHE_EXPIRE = 2;

    @Override
    public Goods queryGoodsByCacheId(Long id) throws BusinessException {
        BoundValueOperations<String, Object> goodsOps =
                redisTemplate.boundValueOps(GOODS_CACHE + id);
        //1.先从缓存中找，如果有就直接返回
        Object goodsCache = goodsOps.get();
        boolean emptyFlag = StringUtils.isEmpty(goodsCache);
        if (!emptyFlag) {
            return JSONUtil.toBean(goodsCache.toString(), Goods.class);
        } else {
            //2.如果没有就从数据库中查找，放入缓存，然后返回
            Goods goods = new Goods();
            goods.setId(id);
            goods.setStatus(1);
            //只查询上架的商品(status)
            Goods goodsBack = goodsMapper.selectOne(goods);
            //没找到，抛业务异常
            if (goodsBack == null) {
                throw new BusinessException("商品不存在！");
            }
            // 转换json、放入缓存, 设置过期时间
            goodsOps.set(Objects.requireNonNull(JSONUtil.toJsonStr(goodsBack))
                    , GOODS_CACHE_EXPIRE, TimeUnit.HOURS);
            //返回结果
            return goodsBack;
        }
    }

    @Override
    public Boolean seckill(Long goodsId, Integer userId) throws BusinessException {
        //秒杀前先刷新缓存，缓存中有则继续，没有则取自mysql
        this.goodsRefreshById(goodsId);
        userId = RandomUtil.randomInt(1, 100);

        // 访问资源时,先通过setnx判断有没有锁(lock)
        //为了让锁在锁资源过程中能够更加稳定被释放,可以利用redis中的过期时间10秒钟
        BoundValueOperations<String, Object> lockOps =
                redisTemplate.boundValueOps("goodsLock");
        // 给key赋值的时候, 通过java提供UUID,把UUID作为lock的value
        String uuid = IdUtil.simpleUUID();
        Boolean goodsLock = lockOps.setIfAbsent(uuid, 10, TimeUnit.SECONDS);
        // 如果加锁失败, 则秒杀失败
        if (goodsLock != null && !goodsLock) return false;
        // 但是, 生成key的操作(setIfAbsent)和过期时间设置的操作expire无法形成原子操作
        // 解决办法: 将上述两个操作形成原子级别的操作(至少springboot2.2.4 和 tomcat9.0.24)

        // 锁的失效可能性: 第一个抢到锁的用户没有完成业务且没有释放(假设15秒才完成),在该过程中,redis系统已经将锁释放,导致第二个用户拿到锁资源,
        // 此时第一个用户执行了删除锁操作,使得第三个用户可以加锁...锁"消失"了
        // 目标: 某个用户加锁,只能他自己解锁
        // 如果还要更高的可用性, 还可以在快要到期的时候,让lock的过期时间延长(通过定时任务Timer)
        // Redisson就是一个强大的锁功能

        // 查被秒杀商品信息，转为java对象，获得其库存数量
        BoundValueOperations<String, Object> goodsOps =
                redisTemplate.boundValueOps(GOODS_CACHE + goodsId);
        Object goodsValue = goodsOps.get();
        Goods goodsBack = JSONUtil.
                toBean(goodsValue != null ? goodsValue.toString() : null, Goods.class);
        // 为了让操作中无论业务是否出现问题,都能够正确释放锁,还需要通过异常处理保证及时释放,防止死锁发送
        try {
            // 判断有没有库存(stock),如果有库存-1
            Integer stock = goodsBack.getNum();
            if (stock > 0) {
                goodsBack.setNum(stock - 1);
                goodsOps.set(JSONUtil.toJsonStr(goodsBack));
                // 用户id添加到成功秒杀用户队列
                redisTemplate.opsForList().leftPush("seckillList", "用户" + userId + "--GOT--商品" + goodsId);
                System.out.println("---Sucess!  用户" + userId + "抢到了《" + goodsBack.getTitle() + "》，库存余" + (stock - 1));
                return true;
            }
        } finally {
            // 释放锁资源
            // 通过判断, 在当前业务中产生的uuid作为lock的value,
            // 和redis服务器中lock的值判断,如果相同,才表示是同一个操作的人,才能释放锁
            if (Objects.requireNonNull(lockOps.get()).toString().equals(uuid)) {
                Boolean isDel = redisTemplate.delete("goodsLock");
                System.out.println("锁资源释放" + (isDel != null && isDel ? "成功!" : "失败!"));
            }
        }
        return false;
    }

    @Override
    public Boolean goodsRefreshById(Long id) throws BusinessException {
        BoundValueOperations<String, Object> goodsOps =
                redisTemplate.boundValueOps(GOODS_CACHE + id);
        Object goodsCache = goodsOps.get();
        boolean emptyFlag = StringUtils.isEmpty(goodsCache);
        if (!emptyFlag) {
            return true;
        } else {
            //2.如果没有就从数据库中查找，放入缓存，然后返回
            Goods goodsBack = goodsMapper.selectByPrimaryKey(id);
            //没找到，抛业务异常
            if (goodsBack == null) {
                throw new BusinessException("商品不存在！");
            }
            // 转换json、放入缓存, 设置过期时间
            goodsOps.set(Objects.requireNonNull(JSONUtil.toJsonStr(goodsBack))
                    , GOODS_CACHE_EXPIRE, TimeUnit.HOURS);
            return true;
        }
    }
}
